Como reproducir audio usando la memoria interna de un microcontrolador AVR.
NOTA: La memoria de un ATMEGA168PA es de 16KB esto nos permite reproducir un total de 1,5 segundos de un archivo wave con una tasa de 8.000 samples por segundo, en el caso de que necesitemos mas tiempo de audio tendriamos que usar algún modulo de memoria o utilizar algún algoritmo de compresion de audio como el DPCM.
Primero necesitamos crear un archivo de cabecera (.h) que contendrá todos los samples de nuestro audio.
El archivo de audio que necesitamos tiene que tener las siguientes caracteristicas:
Para comprobar las caracteristicas de un archivo podemos usar el comando soxi del paquete sox:
soxi audio.wav
La salida es algo como esto:
Si nuestro audio no tiene los parametros correctos, podemos convertirlo usando Audacity, solo tenemos que re-exportar el audio asi:
Una vez tenemos el audio con los parámetros necesarios generamos el archivo de cabecera con los samples, usando el siguiente script de python:
import wave
import sys
def getWaveData(waveFile):
try:
w = wave.Wave_read(waveFile)
data = []
for i in range(w.getnframes()):
data.append(int.from_bytes(w.readframes(1)))
return(data)
except FileNotFoundError:
print("No existe el archivo indicado")
exit()
def createHeader(filename, waveData):
outfile = open(filename[:-4] + ".h", "w")
outfile.write('const uint8_t wave_audio[] PROGMEM = {\n')
for sample in waveData:
outfile.write(' {:d},\n'.format(sample))
outfile.write('};\n')
outfile.close()
if __name__ == "__main__":
if len(sys.argv) == 1:
print("\nERROR: indica un archivo, eg: python " + sys.argv[0] + " audio.wav\n")
exit()
filename = sys.argv[1]
waveData = getWaveData(filename)
createHeader(filename, waveData)
Para ejecutar el script solo tenemos que pasarle el archivo .wav como primer argumento:
python WaveToArray.py hola.wav
El script nos generará un archivo .h como el siguiente:
const uint8_t wave_audio[] PROGMEM = {
130,
122,
116,
111,
107,
101,
97,
94,
94,
99,
114,
132,
154,
168,
173,
166,
150,
133,
120,
...
...
...
(Samples omitidos por brevedad)
}
Cada linea es un sample del audio, y ocupa 1 byte, por lo tanto el numero de lineas que tenga el array es el tamaño del audio en memoria, (para el atmega168pa no debería de ser mayor de 16.000 lineas (16KB))
El código que usaremos en el microcontrolador para reproducir nuestro audio es el siguiente:
#include <avr/io.h>
#include <avr/power.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include "hola.h"
uint16_t sample = 0;
ISR(TIMER2_COMPA_vect) {
sample++;
if(sample > sizeof(wave_audio))
sample = 0;
OCR0A = pgm_read_byte(&(wave_audio[sample]));
}
void initTimer0(void) {
TCCR0A |= (1 << COM0A1); // PWM output on PD6 (OC0A)
TCCR0A |= (1 << WGM00); // Fast PWM mode
TCCR0A |= (1 << WGM01);
TCCR0B |= (1 << CS00); // Clock with /1 prescaler
}
void initTimer2(void) {
TCCR2A |= (1 << WGM21); // modo CTC
TIMSK2 |= (1 << OCIE2A);
TCCR2B |= (1 << CS21); // Clock with /8 prescaler
sei();
OCR2A = 125; // pasar al siguiente sample cada 125 microsegundos (8000 hz)
}
int main(void) {
clock_prescale_set(clock_div_1); //CPU clock 8 MHz
initTimer0();
initTimer2();
DDRD |= (1 << PD6); // PD6 como salida
while (1) {}
return 0;
}
Como se puede ver hemos hecho el #include del archivo de cabecera que hemos generado previamente, "hola.h" en este caso.
El programa usa los timers 0 y 2 para reproducir el sonido, el timer 0 está configurado para generar señales pwm con los samples que hay almacenados en nuestro array de samples, y el timer 2 se encarga de ir actualizando el sample que reproduce el timer 0 usando la velocidad de sampleado de nuestro archivo de audio, en este caso cada 125 microsegundos (8000 hz).
Por lo tanto el timer 0 se encarga de reproducir el sample y el timer 2 se encarga de ir avanzando en el archivo de audio acorde con la velocidad de sampleado del audio (8000hz en nuestro caso).
AVR | Audio